<?php
/*
XML-RPC Server for Textpattern 4.0.x
http://txp.kusor.com/rpc-api
(C)2005-2006 The Textpattern Development Team - http://textpattern.com
@author Pedro Palazón - http://kusor.com
$HeadURL: http://textpattern.googlecode.com/svn/releases/4.5.2/source/rpc/TXP_RPCServer.php $
$LastChangedRevision: 4011 $
 */

if (!defined('txpath')) die('txpath is undefined.');

require_once txpath.'/lib/txplib_html.php';

class TXP_RPCServer extends IXR_IntrospectionServer
{
	function TXP_RPCServer()
	{
		global $enable_xmlrpc_server;

		$this->IXR_IntrospectionServer();
		// Add API Methods as callbacks:

		if ($enable_xmlrpc_server)
		{
			// Blogger API [http://www.blogger.com/developers/api/] - add as server capability
			$this->capabilities['bloggerAPI'] = array(
				'specUrl'     => 'http://www.blogger.com/developers/api/',
				'specVersion' => 2
			);
			$this->addCallback(
				'blogger.newPost',
				'this:blogger_newPost',
				array('int', 'string', 'string', 'string', 'string', 'string', 'boolean'),
				'makes a new post to a designated blog'
			);
			$this->addCallback(
				'blogger.editPost',
				'this:blogger_editPost',
				array('boolean', 'string', 'string', 'string', 'string', 'string', 'boolean'),
				'changes the contents of a given post'
			);
			$this->addCallback(
				'blogger.getUsersBlogs',
				'this:blogger_getUsersBlogs',
				array('struct', 'string', 'string', 'string'),
				'return information about all the blogs a given user is member of'
			);
			$this->addCallback(
				'blogger.getUserInfo',
				'this:blogger_getUserInfo',
				array('struct', 'string', 'string', 'string'),
				'return information about the current user'
			);
			$this->addCallback(
				'blogger.getTemplate',
				'this:blogger_getTemplate',
				array('string', 'string', 'string', 'string', 'string', 'string'),
				'return section template - main will return default template, archiveIndex will return section template'
			);
			$this->addCallback(
				'blogger.setTemplate',
				'this:blogger_setTemplate',
				array('boolean', 'string', 'string', 'string', 'string', 'string', 'string'),
				'updates section template - main=default template, archiveIndex=section template'
			);

			// non-official Blogger API methods - supported by XML-RPC clients as BloggerAPI2
			// Place all this info on a public URI
			$this->addCallback(
				'blogger.getPost',
				'this:blogger_getPost',
				array('struct', 'string', 'string', 'string', 'string'),
				'retrieves contents for the given postid'
			);
			$this->addCallback(
				'blogger.deletePost',
				'this:blogger_deletePost',
				array('boolean', 'string', 'string', 'string', 'string', 'boolean'),
				'deletes a given post'
			);
			$this->addCallback(
				'blogger.getRecentPosts',
				'this:blogger_getRecentPosts',
				array('array', 'string', 'string', 'string', 'string', 'int'),
				'retrieves a list of posts (default 10)'
			);

			// metaWeblog API[http://www.xmlrpc.com/metaWeblogApi] - add as server capability
			$this->capabilities['metaWeblog API'] = array(
				'specUrl'     => 'http://www.xmlrpc.com/metaWeblogApi',
				'specVersion' => 1
			);
			// Implements also MovableType extension of the API methods
			$this->addCallback(
				'metaWeblog.getPost',
				'this:metaWeblog_getPost',
				array('struct', 'string', 'string', 'string'),
				'retrieves contents for the given postid'
			);
			$this->addCallback(
				'metaWeblog.newPost',
				'this:metaWeblog_newPost',
				array('string', 'string', 'string', 'string', 'struct', 'boolean'),
				'creates a new post'
			);
			$this->addCallback(
				'metaWeblog.editPost',
				'this:metaWeblog_editPost',
				array('boolean', 'string', 'string', 'string', 'struct', 'boolean'),
				'creates a new post'
			);
			$this->addCallback(
				'metaWeblog.getCategories',
				'this:metaWeblog_getCategories',
				array('struct', 'string', 'string', 'string'),
				'retrieves a list of categories for the current blog'
			);
			$this->addCallback(
				'metaWeblog.getRecentPosts',
				'this:metaWeblog_getRecentPosts',
				array('array', 'string', 'string', 'string', 'int'),
				'retrieves a given number of recent posts'
			);
#TODO: metaWeblog.newMediaObject (blogid, username, password, struct) returns struct

			// MovableType API[] - add as server capability
			$this->capabilities['MovableType API'] = array(
				'specUrl'     => 'http://www.sixapart.com/movabletype/docs/mtmanual_programmatic.html#xmlrpc%20api',
				'specVersion' => 1
			);
			// Not completelly implemented.
			$this->addCallback(
				'mt.getRecentPostTitles',
				'this:mt_getRecentPostTitles',
				array('array', 'string', 'string', 'string', 'id'),
				'returns a bandwidth-friendly list of the most recent posts in the system'
			);
			$this->addCallback(
				'mt.getCategoryList',
				'this:mt_getCategoryList',
				array('array', 'string', 'string', 'string'),
				'returns a list of all categories defined in the weblog'
			);
			$this->addCallback(
				'mt.supportedMethods',
				'this:listMethods',
				array('array'),
				'return the XML-RPC Methods supported by the server(Redundant).'
			);
			$this->addCallback(
				'mt.supportedTextFilters',
				'this:mt_supportedTextFilters',
				array('array'),
				'return the format filters suported by the server.'
			);
			$this->addCallback(
				'mt.getPostCategories',
				'this:mt_getPostCategories',
				array('array', 'string', 'string', 'string'),
				'returns a list of all categories for the given article'
			);
			$this->addCallback(
				'mt.setPostCategories',
				'this:mt_setPostCategories',
				array('boolean', 'string', 'string', 'string', 'array'),
				'sets categories for the given article'
			);
			$this->addCallback(
				'mt.publishPost',
				'this:mt_publishPost',
				array('boolean', 'string', 'string', 'string'),
				'changes the status of the current article to published'
			);
		}
	}

	// Override serve method in order to keep requests logs too
	// while dealing with unknown clients
	function serve($data = false)
	{
		if (!$data)
		{
			global $HTTP_RAW_POST_DATA;

			if (!$HTTP_RAW_POST_DATA)
			{
				// RSD lets them find us via GET requests
				$this->rsd();
				exit;
			}

			$rx = '/<?xml.*encoding=[\'"](.*?)[\'"].*?>/m';

			// first, handle the known UAs in order to serve proper content
			if (strpos('w.bloggar', $_SERVER['HTTP_USER_AGENT']) !== false)
			{
				$encoding = 'iso-8859-1';
			}
			// find for supplied encoding before to try other things
			elseif (preg_match($rx, $HTTP_RAW_POST_DATA, $xml_enc))
			{
				$encoding = strtolower($xml_enc[1]);
			}
			// try utf-8 detect
			elseif (preg_match('/^([\x00-\x7f]|[\xc2-\xdf][\x80-\xbf]|\xe0[\xa0-\xbf][\x80-\xbf]|[\xe1-\xec][\x80-\xbf]{2}|\xed[\x80-\x9f][\x80-\xbf]|[\xee-\xef][\x80-\xbf]{2}|f0[\x90-\xbf][\x80-\xbf]{2}|[\xf1-\xf3][\x80-\xbf]{3}|\xf4[\x80-\x8f][\x80-\xbf]{2})*$/', $HTTP_RAW_POST_DATA) === 1)
			{
				$encoding = 'utf-8';
			}
			// otherwise, use iso-8859-1
			else
			{
				$encoding = 'iso-8859-1';
			}

			switch ($encoding)
			{
				case 'utf-8':
					$data = $HTTP_RAW_POST_DATA;
					break;

				case 'iso-8859-1':
#TODO: if utf8 conversion fails, throw: 32701 ---> parse error. unsupported encoding?
#see: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
					// this will fail on parser if utf8_encode is unavailiable
					$data = (function_exists('utf8_encode') && is_callable('utf8_encode'))? utf8_encode($HTTP_RAW_POST_DATA) : $HTTP_RAW_POST_DATA;
					break;

				default:
#TODO: if utf8 conversion fails, throw: 32701 ---> parse error. unsupported encoding?
					// this will fail on parser if mb_convert_encoding is unavailiable
					$data = (function_exists('mb_convert_encoding') && is_callable('mb_convert_encoding'))? mb_convert_encoding($HTTP_RAW_POST_DATA, 'utf-8', $encoding) : $HTTP_RAW_POST_DATA;
					break;
			}
		}

		$this->message = new IXR_Message($data);

		if (!$this->message->parse())
		{
			$this->error(-32700, 'parse error. not well formed');
		}

		if ($this->message->messageType != 'methodCall')
		{
			$this->error(-32600, 'server error. invalid xml-rpc. not conforming to spec. Request must be a methodCall');
		}

		$result = $this->call($this->message->methodName, $this->message->params);

		// Is the result an error?
		if (is_a($result, 'IXR_Error'))
		{
			$this->error($result);
		}

		// Encode the result
		$r = new IXR_Value($result);
		$resultxml = $r->getXml();

		// Create the XML
		$xml = <<<EOD
<methodResponse>
	<params>
		<param>
			<value>
				$resultxml
			</value>
		</param>
	</params>
</methodResponse>

EOD;

		return $this->output($xml, $encoding);
	}

	// Override default utf-8 output, if needed
	function output($xml, $enc = 'utf-8')
	{
		// Be kind with non-utf-8 capable clients
		if ($enc != 'utf-8')
		{
			if ($enc == 'iso-8859-1' && function_exists('utf8_decode') && is_callable('utf8_decode'))
			{
				$xml = utf8_decode($xml);
			}
			elseif (function_exists('mb_convert_encoding') && is_callable('mb_convert_encoding'))
			{
				$xml = mb_convert_encoding($xml, $enc, 'utf-8');
			}
			else
			{
# TODO: shouldn't this throw an error instead of serving non-utf8 content as utf8?
				// if no decoding possible, serve contents as utf-8
				$enc = 'utf-8';
			}
		}

		$xml = '<?xml version="1.0" encoding="'.$enc.'" ?>'.n.$xml;
		$length = strlen($xml);
		header('Connection: close');
		header('Content-Length: '.$length);
		header('Content-Type: text/xml charset='.$enc);
		header('Date: '.date('r'));
		echo $xml;
		exit;
	}

//---------------------------------------------------------
// Really Simple Discoverability 1.0 response

	function rsd()
	{
		global $enable_xmlrpc_server;

		if ($enable_xmlrpc_server)
		{
			$this->output(
				tag(
					n.tag(
						n.tag('Textpattern', 'engineName').
						n.tag('http://textpattern.com/', 'engineLink').
						n.tag(hu, 'homePageLink').
						n.tag(
							n.'<api name="Movable Type" blogID="" preferred="true" apiLink="'.txrpcpath.'" />'.
							n.'<api name="MetaWeblog" blogID="" preferred="false" apiLink="'.txrpcpath.'" />'.
							n.'<api name="Blogger" blogID="" preferred="false" apiLink="'.txrpcpath.'" />'.n,
							'apis').n,
					'service').n,
					'rsd', ' version="1.0" xmlns="http://archipelago.phrasewise.com/rsd"')
				);
		}
		else
		{
			header('Status: 501 Not Implemented');
			header('HTTP/1.1 501 Not Implemented');
			die('Not Implemented');
		}
	}

//---------------------------------------------------------
// Blogger API

	function blogger_newPost($params)
	{
		list($appkey, $blogid, $username, $password, $content, $publish) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$contents = $this->_getBloggerContents($content);

		$contents['Section'] = $blogid;
		$contents['Status']  = $publish ? '4' : '1';

		$rs = $txp->newArticle($contents);

		if (false === $rs)
		{
			return new IXR_Error(201, gTxt('problem_creating_article'));
		}

		return intval($rs);
	}

	function blogger_editPost($params)
	{
		list($appkey, $postid, $username, $password, $content, $publish) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$id = $txp->getArticleID($postid, 'ID');

		if (!$id)
		{
			return new IXR_Error(404, gTxt('invalid_article_id'));
		}

		$contents = $this->_getBloggerContents($content);

		$contents['Status'] = $publish ? '4' : '1';

		$rs = $txp->updateArticleID($postid, $contents);

		if (false === $rs)
		{
			return new IXR_Error(202, gTxt('problem_updating_article'));
		}

		return true;
	}

	function blogger_getUsersBlogs($params)
	{
		list($appkey, $username, $password) = $params;

		global $permlink_mode;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$rs = $txp->getSectionsList();

		if (false === $rs)
		{
			return new IXR_Error(203, gTxt('problem_retrieving_sections'));
		}

		$sections = array();

		foreach ($rs as $section)
		{
			$sections[] = array(
				'blogid'   => $section['name'],
				'blogName' => $section['title'],
				'url'      => pagelinkurl(array('s' => $section['name']))
			);
		}

		return $sections;
	}

	function blogger_getUserInfo($params)
	{
		list($appkey, $username, $password) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$rs = $txp->getUser();

		if (!$rs)
		{
			return new IXR_Error(204, gTxt('unable_retrieve_user'));
		}

		extract($rs);

		if (strpos($RealName, ' ') != 0)
		{
			list($firstname, $lastname) = explode(' ', $RealName);
		}
		else
		{
			$firstname = $RealName;
			$lastname  = '';
		}

		$uinfo = array(
			'userid'    => $user_id,
			'firstname' => $firstname,
			'lastname'  => $lastname,
			'nickname'  => $name,
			'email'     => $email,
			'url'       => hu
		);

		return $uinfo;
	}

	function blogger_getTemplate($params)
	{
		list($appkey, $blogid, $username, $password, $templateType) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		if ($templateType == 'archiveIndex' && $blogid != 'default')
		{
			$section = $txp->getSection($blogid);

			if (!$section)
			{
				return new IXR_Error(208, gTxt('unable_retrieve_template'));
			}

			$name = $section['page'];
		}
		else
		{
			$name = 'default';
		}

		$rs = $txp->getTemplate($name);

		if (!$rs)
		{
			return new IXR_Error(208, gTxt('unable_retrieve_template'));
		}

		return $rs;
	}

	function blogger_setTemplate($params)
	{
		list($appkey, $blogid, $username, $password, $template, $templateType) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		if ($templateType == 'archiveIndex' && $blogid != 'default')
		{
			$section = $txp->getSection($blogid);

			if (!$section)
			{
				return new IXR_Error(209, gTxt('unable_set_template'));
			}

			$name = $section['page'];
		}
		else
		{
			$name = 'default';
		}

		$rs = $txp->setTemplate($name, $template);

		if (!$rs)
		{
			return new IXR_Error(209, gTxt('unable_set_template'));
		}

		return true;
	}

//---------------------------------------------------------
// Blogger 2.0

	function blogger_getPost($params)
	{
		list($appkey, $postid, $username, $password) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$rs = $txp->getArticleID($postid, 'ID, Body, AuthorId, unix_timestamp(Posted) as uPosted');

		if (!$rs)
		{
			return new IXR_Error(205, gTxt('problem_retrieving_article'));
		}

		$out = array(
			'content'     => $rs['Body'],
			'userId'      => $rs['AuthorId'],
			'postId'      => $rs['ID'],
			'dateCreated' => new IXR_Date($rs['uPosted'] + tz_offset())
		);

		return $out;
	}

	function blogger_deletePost($params)
	{
		list($appkey, $postid, $username, $password, $publish) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		// Always delete, no matter of publish
		$rs = $txp->deleteArticleID($postid);

		if (!$rs)
		{
			return new IXR_Error(206, gTxt('problem_deleting_article'));
		}

		return $rs;
	}

	function blogger_getRecentPosts($params)
	{
		list($appkey, $blogid, $username, $password, $numberOfPosts) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$articles = $txp->getArticleList('ID, Body, AuthorId, unix_timestamp(Posted) as uPosted', "Section='".doSlash($blogid)."'", '0', $numberOfPosts, false);

		if (false === $articles)
		{
			return new IXR_Error(207, gTxt('problem_getting_articles'));
		}

		foreach ($articles as $rs)
		{
			$out[] = array(
				'content'     => $rs['Body'],
				'userId'      => $rs['AuthorId'],
				'postId'      => $rs['ID'],
				'dateCreated' => new IXR_Date($rs['uPosted'] + tz_offset())
			);
		}
		return $out;
	}

//---------------------------------------------------------
// metaWeblog API

	function metaWeblog_getPost($params)
	{
		list($postid, $username, $password) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$rs = $txp->getArticleID($postid, 'ID, Title, Body, Excerpt, Annotate, Keywords, Section, Category1, Category2, textile_body, url_title, unix_timestamp(Posted) as uPosted');

		if (!$rs)
		{
			return new IXR_Error(205, gTxt('problem_retrieving_article'));
		}

		return $this->_buildMetaWeblogStruct($rs, $txp);
	}

	function metaWeblog_newPost($params)
	{
		list($blogid, $username, $password, $struct, $publish) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$contents = $this->_getMetaWeblogContents($struct, $publish, $txp);

		$contents['Section'] = $blogid;

		$rs = $txp->newArticle($contents);

		if (false === $rs)
		{
			return new IXR_Error(201, gTxt('problem_creating_article'));
		}

#TODO: why "" quoted $rs?
		return "$rs";
	}

	function metaWeblog_editPost($params)
	{
		list($postid, $username, $password, $struct, $publish) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$contents = $this->_getMetaWeblogContents($struct, $publish, $txp);

		$rs = $txp->updateArticleID($postid, $contents);

		if (false === $rs)
		{
			return new IXR_Error(201, gTxt('problem_updating_article'));
		}

		return true;
	}

	function metaWeblog_getCategories($params)
	{
		list($blogid, $username, $password) = $params;

		global $permlink_mode;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$rs = $txp->getCategoryList();

		if (false === $rs)
		{
			return new IXR_Error(210, gTxt('problem_retrieving_categories'));
		}

		$cats = array();

		foreach ($rs as $c)
		{
			$cats[] = array(
				'categoryName' => $c['title'],
				'description'  => $c['title'],
				'htmlUrl'      => pagelinkurl(array('c' => $c['name'])),
				'rssUrl'       => hu.'?rss=1&#38;category='.$c['name']
			);
		}

		return $cats;
	}

	function metaWeblog_getRecentPosts($params)
	{
		list($blogid, $username, $password, $numberOfPosts) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$articles = $txp->getArticleList('ID, Title, url_title, Body, Excerpt, Annotate, Keywords, Section, Category1, Category2, textile_body, AuthorID, unix_timestamp(Posted) as uPosted',
			"Section='".doSlash($blogid)."'", '0', $numberOfPosts, false);

		if (false === $articles)
		{
			return new IXR_Error(207, gTxt('problem_getting_articles'));
		}

		$out = array();

		foreach ($articles as $rs)
		{
			$out[] = $this->_buildMetaWeblogStruct($rs, $txp);
		}

		return $out;
	}

//---------------------------------------------------------
// MovableType API

	function mt_getRecentPostTitles($params)
	{
		list($blogid, $username, $password, $numberOfPosts) = $params;

		global $prefs;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$articles = $txp->getArticleList('ID, Title, AuthorID, unix_timestamp(Posted) as uPosted', "Section='".doSlash($blogid)."'", '0', $numberOfPosts, false);

		if (false === $articles)
		{
			return new IXR_Error(207, gTxt('problem_getting_articles'));
		}

		extract($prefs);

		$out = array();

		foreach ($articles as $rs)
		{
			$out[] = array(
				'userid'      => $username,
				'postid'      => $rs['ID'],
				'dateCreated' => new IXR_Date($rs['uPosted'] + tz_offset()),
				'title'       => $rs['Title'],
			);
		}

		return $out;
	}

	function mt_getCategoryList($params)
	{
		list($blogid, $username, $password) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$rs = $txp->getCategoryList();

		if (false === $rs)
		{
			return new IXR_Error(210, gTxt('problem_retrieving_categories'));
		}

		$cats = array();

		foreach ($rs as $c)
		{
			$cats[] = array(
				'categoryName' => $c['title'],
				'categoryId'   => $c['id'],
			);
		}

		return $cats;
	}

	function mt_supportedTextFilters($params)
	{
		$filters = array(
			array('key' => '0', 'label' => 'LEAVE_TEXT_UNTOUCHED'),
			array('key' => '1', 'label' => 'USE_TEXTILE'),
			array('key' => '2', 'label' => 'CONVERT_LINEBREAKS'),
		);

		return $filters;
	}

	function mt_getPostCategories($params)
	{
		list($postid, $username, $password) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$post = $txp->getArticleID($postid, 'Category1, Category2');

		if (!$post)
		{
			return new IXR_Error(211, gTxt('problem_retrieving_article_categories'));
		}

		$out = array();
		$isPrimary = true;
		$cats[] = $post['Category1'];
		$cats[] = $post['Category2'];

		foreach ($cats as $category)
		{
			if (!empty($category))
			{
				$rs = $txp->getCategory($category);

#TODO: remove?
				// if (!$rs) return new IXR_Error(212, gTxt('problem_retrieving_category_info'));

				$ct['categoryId']   = $rs['id'];
				$ct['categoryName'] = $rs['title'];
				$ct['isPrimary']    = $isPrimary;

				$out[] = $ct;
			}

			if ($isPrimary) $isPrimary = false;
		}

		return $out;
	}

#TODO: explain what 'expecific' is ;)
	// supported to avoid some client expecific behaviour
	function mt_publishPost($params)
	{
		list($postid, $username, $password) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$published = $txp->updateArticleField($postid, 'Status', '4');

		if (!$published)
		{
			return new IXR_Error(201, gTxt('problem_updating_article'));
		}

		return true;
	}

	function mt_setPostCategories($params)
	{
		list($postid, $username, $password, $categories) = $params;

		$txp = new TXP_Wrapper($username, $password);

		if (!$txp->loggedin)
		{
			return new IXR_Error(100, gTxt('bad_login'));
		}

		$Category1 = '';
		$Category2 = '';

		foreach ($categories as $category)
		{
			extract($category);

			$rs = $txp->getCategoryId($categoryId);

			if (!$rs)
			{
				return new IXR_Error(213, gTxt('trying_to_assign_unexisting_category_to_the_article'));
			}

			if (empty($Category1))
			{
				$Category1 = $rs['name'];
			}
			else
			{
				$Category2 = $rs['name'];
			}
		}

		$ct1 = $txp->updateArticleField($postid, 'Category1', $Category1);
		$ct2 = $txp->updateArticleField($postid, 'Category2', $Category2);

		if (!$ct1 || !$ct2)
		{
			return new IXR_Error(214, gTxt('problem_saving_article_categories'));
		}

		return true;

	}

#TODO ???
	// MediaObjects
	/*
	 metaWeblog.newMediaObject
	 Description: Uploads a file to your webserver.
	 Parameters: String blogid, String username, String password, struct file
	 Return value: URL to the uploaded file.
	 Notes: the struct file should contain two keys: base64 bits (the base64-encoded contents of the file)
	 and String name (the name of the file). The type key (media type of the file) is currently ignored.
	 */

	//code refactoring for blogger_newPost & blogger_editPost
	function _getBloggerContents($content)
	{
		$body = $content;

		// Trick to add title, category and excerpts using XML-RPC
		if (preg_match('/<title>(.*)<\/title>(.*)/s', $content, $matches))
		{
			$body  = $matches[2];
			$title = $matches[1];
		}

		$contents = array(
			'Body' => str_replace('\n', n, $body)
		);

		if (isset($title)) $contents['Title'] = $title;

		return $contents;
	}

	//code refactoring for metaWeblog_newPost & metaweblog_EditPost
	function _getMetaWeblogContents($struct, $publish, $txp)
	{
		global $gmtoffset, $is_dst;

		$contents = array(
			'Body'   => str_replace('\n', n, $struct['description']),
			'Status' => $publish ? '4' : '1',
			'Title'  => $struct['title']
		);

		if (!empty($struct['categories']))
		{
			if (!empty($struct['categories'][0]))
			{
				$c = $txp->getCategoryTitle($struct['categories'][0]);
				$contents['Category1'] = $c['name'];
			}

			if (!empty($struct['categories'][1]))
			{
				$c = $txp->getCategoryTitle($struct['categories'][1]);
				$contents['Category2'] = $c['name'];
			}
		}

		if (isset($struct['date_created_gmt']))
		{
			$struct['dateCreated'] = $struct['date_created_gmt'];
			$struct['dateCreated']->tz = 'Z'; // force GMT timezone
		}

		if (isset($struct['dateCreated']))
		{
			if ($struct['dateCreated']->tz == 'Z')
			{
				// GMT-based posting time; transform into server time zone
				$posted = $struct['dateCreated']->getTimestamp() - tz_offset() + $gmtoffset + ($is_dst ? 3600 : 0);
			}
			elseif (!$struct['dateCreated']->tz)
			{
				// posting in an unspecified time zone: Assume site time.
				$posted = $struct['dateCreated']->getTimestamp() - tz_offset();
			}
			else
			{
				// numeric time zone offsets
				if (preg_match('/([+-][0-9]{2})([0-9]{2})/', $struct['dateCreated']->tz, $t))
				{
					$tz = $t[1] * 3600 + $t[2] * 60;
					$posted = $struct['dateCreated']->getTimestamp() - tz_offset() + $gmtoffset + ($is_dst ? 3600 : 0) - $tz;
				}
			}
		}

		if (isset($posted)) $contents['Posted'] = date('Y-m-d H:i:s', $posted);

		// MovableType Implementation Add ons
		if (isset($struct['mt_allow_comments']))
		{
			$contents['Annotate'] = $struct['mt_allow_comments'];
		}

		if (isset($struct['mt_convert_breaks']))
		{
			$contents['textile_body'] = $contents['textile_excerpt'] = intval($struct['mt_convert_breaks']);
		}

		if (isset($struct['mt_text_more']))
		{
			$contents['Body'] .= n.n.str_replace('\n', n ,$struct['mt_text_more']);
		}

		if (isset($struct['mt_excerpt']))
		{
			$contents['Excerpt'] = str_replace('\n', n, $struct['mt_excerpt']);
		}

		if (isset($struct['mt_keywords']))
		{
			$contents['Keywords'] = $struct['mt_keywords'];
		}

		if (isset($struct['mt_basename']))
		{
			$contents['url_title'] = stripSpace($struct['mt_basename']);
		}
		elseif (isset($struct['wp_slug']))
		{
			$contents['url_title'] = stripSpace($struct['wp_slug']);
		}

		return $contents;
	}

	// common code to metaWeblog_getPost and metaWeblog_getRecentPosts
	// could not be this placed on a different file from taghandlers?
	// remove if it is the case
	function _buildMetaWeblogStruct($rs, $txp)
	{
		global $permlink_mode, $is_dst, $gmtoffset;

		switch ($permlink_mode)
		{
			case 'section_id_title':
				$url = hu.join('/', array($rs['Section'], $rs['ID'], $rs['url_title']));
				break;

			case 'year_month_day_title':
				$url = hu.join('/', array(
					date("Y", $rs['uPosted']),
					date("m", $rs['uPosted']),
					date("d", $rs['uPosted']),
					$rs['url_title']
				));
				break;

			case 'title_only':
				$url = hu.$rs['url_title'];
				break;

			case 'section_title':
				$url = hu.join('/', array($rs['Section'], $rs['url_title']));
				break;

			case 'id_title':
				$url = hu.join('/', array($rs['ID'], $rs['url_title']));
				break;

			default:
				//assume messy mode?
				$url = hu.'?id='.$rs['ID'];
				break;
		}

		$cat  = $txp->getCategory($rs['Category1']);
		$cat1 = $cat['title'];
		$cat  = $txp->getCategory($rs['Category2']);
		$cat2 = $cat['title'];

		$out = array(
			'categories'  => array($cat1, $cat2),
			'description' => $rs['Body'],
			'userid'      => $txp->txp_user,
			'postid'      => $rs['ID'],
			'dateCreated' => new IXR_Date($rs['uPosted'] + tz_offset() - $gmtoffset - ($is_dst ? 3600 : 0)),
			'link'        => $url,
			'permaLink'   => $url,
			'title'       => $rs['Title'],
		);

		$out['dateCreated']->tz = 'Z'; // GMT

		// MovableType Implementation Add ons
		if (isset($rs['Annotate']) && !empty($rs['Annotate']))
		{
			$out['mt_allow_comments'] = intval($rs['Annotate']);
		}

		if (isset($rs['textile_body']) && !empty($rs['textile_body']))
		{
			$out['mt_convert_breaks'] = strval($rs['textile_body']);
		}

		if (isset($rs['Excerpt']) && !empty($rs['Excerpt']))
		{
			$out['mt_excerpt'] = $rs['Excerpt'];
		}

		if (isset($rs['Keywords']) && !empty($rs['Keywords']))
		{
			$out['mt_keywords'] = $rs['Keywords'];
		}

		if (isset($rs['url_title']) && !empty($rs['url_title']))
		{
			$out['mt_basename'] = $out['wp_slug'] = $rs['url_title'];
		}

		return $out;
	}
}

?>
